package net.avcompris.base.testutil.processes;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.toArray;
import static org.apache.commons.lang3.CharEncoding.UTF_8;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.io.FileUtils;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

/**
 * This is a base class for JUnit test classes that want to run a or several
 * processes before or while running tests. Mainly, processes consist in reading
 * an entire database or a large
 * 
 * @author David Andrianavalontsalama
 */
public abstract class TestsWithProcessesBefore {

    private final Method[] testMethods;

    protected TestsWithProcessesBefore() {

        final Class<? extends TestsWithProcessesBefore> thisClass = getClass();

        final RequiresProcesses processBefore = thisClass.getAnnotation(RequiresProcesses.class);

        if (processBefore == null) {

            throw new RuntimeException("Test class should have annotation @ProcessBefore: " + thisClass.getName());
        }

        final Class<?>[] processClasses = processBefore.value();

        for (final Class<?> processClass : processClasses) {

            if (!AbstractProcess.class.isAssignableFrom(processClass)) {

                throw new IllegalArgumentException("Declared process class should extend AbstractProcess: "
                        + processClass.getName());
            }

            @SuppressWarnings("unchecked")
            final Class<? extends AbstractProcess<?, ?>> typedClass = (Class<? extends AbstractProcess<?, ?>>) processClass;

            this.processClasses.add(typedClass);
        }

        final List<Method> methods = new ArrayList<Method>();

        for (final Method method : thisClass.getMethods()) {

            if (method.isAnnotationPresent(Test.class) && !method.isAnnotationPresent(Ignore.class)) {

                final Class<?>[] paramTypes = method.getParameterTypes();

                if (paramTypes == null || paramTypes.length == 0) {

                    methods.add(method);
                }
            }
        }

        testMethods = toArray(methods, Method.class);

        storeInstance();
    }

    private void storeInstance() {

        final TestsWithProcessesBefore cachedInstance = instances.get(getClass());

        if (cachedInstance == null) {

            try {

                createInstance(this);

            } catch (final IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (final InstantiationException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private static synchronized void createInstance(final TestsWithProcessesBefore instance)
            throws InstantiationException, IllegalAccessException {

        final Class<? extends TestsWithProcessesBefore> instanceClass = instance.getClass();

        final TestsWithProcessesBefore cachedInstance = instances.get(instanceClass);

        if (cachedInstance != null) {

            return;
        }

        instances.put(instanceClass, instance);

        try {

            instance.initProcess();

        } catch (final Throwable e) {

            e.printStackTrace();

            instance.releaseProcess();
        }
    }

    private void initProcess() throws Exception {

        for (final Class<? extends AbstractProcess<?, ?>> processClass : processClasses) {

            final String processName = processClass.getSimpleName();

            System.out.println("Creating process instance: " + processName + "...");

            final AbstractProcess<?, ?> process = processClass.newInstance();

            System.out.println("Initializing process: " + processName + "...");

            process.setTests(this);

            process.init();

            System.out.println("Process " + processName + " initialized.");

            final ProcessInstance processInstance = new ProcessInstance(process);

            processInstances.put(processClass, processInstance);

            for (final Method testMethod : testMethods) {

                final String methodName = testMethod.getName();

                processInstance.methodErrors.put(methodName, new ArrayList<Throwable>());

                processInstance.methodFailures.put(methodName, new ArrayList<AssertionError>());

                processInstance.processEntryResults.put(methodName, new ArrayList<ProcessEntryResult>());
            }
        }
    }

    private synchronized void releaseProcess() {

        final List<Class<? extends AbstractProcess<?, ?>>> processClassesCopy = new ArrayList<Class<? extends AbstractProcess<?, ?>>>(
                processClasses);

        for (final Class<? extends AbstractProcess<?, ?>> processClass : processClassesCopy) {

            final ProcessInstance processInstance = processInstances.get(processClass);

            if (processInstance == null) {

                continue;
            }

            final AbstractProcess<?, ?> process = processInstance.process;

            final String processName = processClass.getSimpleName();

            System.out.println("Releasing process: " + processName + "...");

            boolean released = true;

            try {

                process.release();

            } catch (final Throwable e) {

                if (processInstance.errors != null) {

                    processInstance.errors.add(e);
                }

                e.printStackTrace();

                released = false;

                System.out.println("ERROR. While releasing process " + processName + ".");
            }

            if (released) {

                System.out.println("Process " + processName + " released.");

                processClasses.remove(processClass);
            }
        }
    }

    private final List<Class<? extends AbstractProcess<?, ?>>> processClasses = new ArrayList<Class<? extends AbstractProcess<?, ?>>>();

    private static Map<Class<? extends TestsWithProcessesBefore>, TestsWithProcessesBefore> instances = new HashMap<Class<? extends TestsWithProcessesBefore>, TestsWithProcessesBefore>();

    private final Map<Class<? extends AbstractProcess<?, ?>>, ProcessInstance> processInstances = new HashMap<Class<? extends AbstractProcess<?, ?>>, ProcessInstance>();

    protected final boolean isProcessRunning() {

        return running;
    }

    private static ParameterizedType getParameterizedTypeSuperclass(final Class<? extends AbstractProcess<?, ?>> c) {

        for (Type k = c.getGenericSuperclass(); k != null;) {

            if (ParameterizedType.class.isInstance(k)) {

                return (ParameterizedType) k;
            }

            k = ((Class<?>) k).getGenericSuperclass();
        }

        throw new RuntimeException("Not implemented.");
    }

    protected final <X> X getProcessCurrentOfType(final Class<X> currentClass) throws Exception {

        checkNotNull(currentClass, "currentClass");

        for (final Class<? extends AbstractProcess<?, ?>> c : getProcessInstances().keySet()) {

            final ParameterizedType parameterizedType = getParameterizedTypeSuperclass(c);

            final Type currentType = parameterizedType.getActualTypeArguments()[0];

            if (areSameTypes(currentClass, currentType)) {

                @SuppressWarnings("unchecked")
                final X x = getProcessCurrent((Class<? extends AbstractProcess<X, ?>>) c);

                return x;
            }
        }

        throw new IllegalArgumentException("Cannot find process class for currentClass: " + currentClass.getName());
    }

    protected final void ignoreThisOneInProcess() {

        ignoreThisOnesThreadLocal.set(true);
    }

    private String getCurrentTestMethodName() {

        for (final StackTraceElement ste : new Exception().getStackTrace()) {

            final String methodName = ste.getMethodName();

            // TODO optimize this! With methodNames put in cache.
            for (final Method testMethod : testMethods) {

                if (methodName.equals(testMethod.getName())) {

                    return methodName;
                }
            }
        }

        return null;
    }

    private static String composeTestSummary(final ProcessInstance processInstance, final String testMethodName) {

        final int count = processInstance.process.getCurrentIndex();

        final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);

        final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);

        final String summary = "Runs: " + count //
                + ", Failures: " + methodFailures.size() //
                + ", Errors: " + methodErrors.size() //
                + " (+" + processInstance.errors.size() + ")";

        return summary;
    }

    private static <T extends Throwable> T recomposeThrowable(final T error, final String summary) {

        final StackTraceElement[] stackTrace = error.getStackTrace();

        T newError = error;

        final Class<? extends Throwable> errorClass = error.getClass();

        Constructor<? extends Throwable> constructor;

        try {

            constructor = errorClass.getConstructor(String.class);

        } catch (final Throwable e1) {

            try {

                constructor = errorClass.getConstructor(Object.class);

            } catch (final Throwable e2) {

                try {

                    constructor = errorClass.getConstructor(String.class, String.class, String.class);

                } catch (final Throwable e3) {

                    constructor = null;
                }
            }
        }

        if (constructor != null) {

            final String message = summary + ", Sample: " + error.getMessage();

            try {

                final Object o;

                if (constructor.getParameterTypes().length == 1) {

                    o = constructor.newInstance(message);

                } else {

                    o = constructor.newInstance(message, "", "");
                }

                @SuppressWarnings("unchecked")
                final T e = (T) o;

                newError = e;

            } catch (final Throwable e) {

                // do nothing
            }
        }

        newError.setStackTrace(stackTrace);

        return newError;
    }

    private final ProcessInstance getUniqueProcessInstance() {

        final int instanceCount = processInstances.size();

        switch (instanceCount) {
        case 0:
            throw new IllegalStateException("Cannot call getUniqueProcessInstance()" //
                    + " with zero process.");

        case 1:
            return processInstances.values().iterator().next();

        default:
            break;
        }

        for (final Method testMethod : testMethods) {

            final WhileProcessing processing = testMethod.getAnnotation(WhileProcessing.class);

            if (processing != null) {

                final Class<? extends AbstractProcess<?, ?>>[] processClasses = processing.value();

                if (processClasses != null && processClasses.length == 1) {

                    return getInitializedProcessInstance(processClasses[0]);
                }
            }
        }

        throw new IllegalStateException("Cannot call getUniqueProcessInstance()" + " with more than one process: "
                + processInstances.keySet().iterator().next().getSimpleName() + ", etc.");
    }

    protected final int getProcessCurrentIndex() throws Exception {

        final ProcessInstance processInstance = getUniqueProcessInstance();

        return getProcessCurrentIndex(processInstance);
    }

    protected final int getProcessCurrentIndex(final Class<? extends AbstractProcess<?, ?>> processClass)
            throws Exception {

        final ProcessInstance processInstance = getInitializedProcessInstance(processClass);

        return getProcessCurrentIndex(processInstance);
    }

    private final int getProcessCurrentIndex(final ProcessInstance processInstance) throws Exception {

        return processInstance.process.getCurrentIndex();
    }

    protected final <X> X getProcessCurrent(final Class<? extends AbstractProcess<X, ?>> processClass) throws Exception {

        checkNotNull(processClass, "processClass");

        final ProcessInstance processInstance = getInitializedProcessInstance(processClass);

        if (!running) {

            final String testMethodName = getCurrentTestMethodName();

            if (testMethodName == null) {
                throw new IllegalStateException("Calling getProcessCurrent() from outside a test method.");
            }

            final String summary = composeTestSummary(processInstance, testMethodName);

            Throwable error = null;

            final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);

            if (!methodErrors.isEmpty()) {

                error = methodErrors.get(0);
            }

            if (error == null) {

                final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);

                if (!methodFailures.isEmpty()) {

                    final AssertionError failure = methodFailures.get(0);

                    throw recomposeThrowable(failure, summary);
                }

                if (!processInstance.errors.isEmpty()) {

                    error = processInstance.errors.get(0);
                }
            }

            if (error != null) {

                if (Error.class.isInstance(error)) {

                    throw recomposeThrowable((Error) error, summary);

                } else if (Exception.class.isInstance(error)) {

                    throw recomposeThrowable((Exception) error, summary);

                } else {

                    throw new RuntimeException(summary, error);
                }
            }
        }

        @SuppressWarnings("unchecked")
        final X current = (X) processInstance.process.getCurrent();

        return current;
    }

    private static boolean areSameTypes(final Class<?> c, final Type t) {

        if (c.equals(t)) {

            return true;
        }

        if (c.isArray() && GenericArrayType.class.isInstance(t)) {

            if (c.getComponentType().equals(((GenericArrayType) t).getGenericComponentType())) {

                return true;
            }
        }

        return false;
    }

    protected final <X> X getProcessResultOfType(final Class<?> resultClass) throws Exception {

        checkNotNull(resultClass, "resultClass");

        for (final Class<? extends AbstractProcess<?, ?>> c : getProcessInstances().keySet()) {

            final ParameterizedType parameterizedType = getParameterizedTypeSuperclass(c);

            final Type resultType = parameterizedType.getActualTypeArguments()[1];

            if (areSameTypes(resultClass, resultType)) {

                @SuppressWarnings("unchecked")
                final X x = getProcessResult((Class<? extends AbstractProcess<?, X>>) c);

                return x;
            }
        }

        throw new IllegalArgumentException("Cannot find process class for resultClass: " + resultClass.getName());
    }

    protected final <X> X getProcessResult(final Class<? extends AbstractProcess<?, X>> processClass) throws Exception {

        checkNotNull(processClass, "processClass");

        if (running || lockFile.exists()) {

            throw new IllegalStateException("Process are still running.");
        }

        final ProcessInstance processInstance = getInitializedProcessInstance(processClass);

        if (!processInstance.errors.isEmpty()) {

            final Throwable error = processInstance.errors.get(0);

            if (Error.class.isInstance(error)) {

                throw (Error) error;

            } else if (Exception.class.isInstance(error)) {

                throw (Exception) error;

            } else {

                throw new RuntimeException(error);
            }
        }

        @SuppressWarnings("unchecked")
        final X result = (X) processInstance.process.getResult();

        return result;
    }

    private static boolean running = false;

    @Before
    public final void setUpProcess() throws Exception {

        if (!running) {

            runProcess();
        }
    }

    final File lockFile = new File("target", getClass().getSimpleName() + ".lock");

    private static Set<Class<? extends TestsWithProcessesBefore>> haveBeenRun = new HashSet<Class<? extends TestsWithProcessesBefore>>();

    private synchronized void runProcess() throws IOException, IllegalArgumentException, SecurityException,
            InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        if (running) {

            return;
        }

        if (lockFile.exists()) {

            throw new IllegalStateException("Lock file exists (" + new DateTime(lockFile.lastModified()) + "): "
                    + lockFile.getCanonicalPath());
        }

        final Class<? extends TestsWithProcessesBefore> thisClass = getClass();

        if (haveBeenRun.contains(thisClass)) {

            return;
        }

        FileUtils.touch(lockFile);

        running = true;

        haveBeenRun.add(thisClass);

        runInstanceProcess();

        lockFile.delete();

        running = false;
    }

    private ProcessInstance getInitializedProcessInstance(final Class<? extends AbstractProcess<?, ?>> processClass) {

        final String processName = processClass.getSimpleName();

        final ProcessInstance processInstance = getProcessInstances().get(processClass);

        if (processInstance == null) {
            throw new IllegalStateException("Process has not been initialized: " + processName);
        }

        return processInstance;
    }

    private Map<Class<? extends AbstractProcess<?, ?>>, ProcessInstance> getProcessInstances() {

        final Class<? extends TestsWithProcessesBefore> thisClass = getClass();

        final TestsWithProcessesBefore instance = instances.get(thisClass);

        if (instance == null) {
            throw new IllegalStateException("Instance has not been initialized for: " + thisClass);
        }

        return instance.processInstances;
    }

    private ProcessInstance getInitializedProcessInstance(final AbstractProcess<?, ?> process) {

        final String processName = process.getClass().getSimpleName();

        for (final Map.Entry<Class<? extends AbstractProcess<?, ?>>, ProcessInstance> entry : processInstances
                .entrySet()) {

            final Class<?> c = entry.getKey();

            if (c.equals(process.getClass())) {

                return entry.getValue();
            }
        }

        throw new IllegalStateException("Process has not been initialized: " + processName);
    }

    private void runInstanceProcess() throws IOException, IllegalArgumentException, SecurityException,
            InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        try {

            for (final Class<? extends AbstractProcess<?, ?>> processClass : processClasses) {

                final String processName = processClass.getSimpleName();

                final ProcessInstance processInstance = getInitializedProcessInstance(processClass);

                System.out.println("Executing process: " + processName + "...");

                final long start = System.currentTimeMillis();

                assertTrue("Error list is not empty.", processInstance.errors.isEmpty());

                for (final Method testMethod : testMethods) {

                    final String testMethodName = testMethod.getName();

                    assertTrue("Error list is not empty for: " + testMethodName + "()", processInstance.methodErrors
                            .get(testMethodName).isEmpty());

                    assertTrue("Failure list is not empty for: " + testMethodName + "()",
                            processInstance.methodFailures.get(testMethodName).isEmpty());
                }

                boolean success = false;

                try {

                    processInstance.process.execute();

                    success = true;

                } catch (final Throwable e) {

                    success = false;

                    processInstance.errors.add(e);

                    e.printStackTrace(System.out);
                }

                final long elapsed = System.currentTimeMillis() - start;

                System.out.println();

                if (success) {

                    System.out.print("SUCCESS. Process " + processName + " done in " + elapsed + " ms.");

                } else {

                    System.out.print("ERROR. In process " + processName + " after " + elapsed + " ms.");
                }

                processInstance.process.cliProgress.end();

                // System.out.println();

                emitProcessEntryReport(processInstance);

                emitStandardReport(processInstance);
            }

        } finally {

            releaseProcess();
        }
    }

    private static final ThreadLocal<Boolean> ignoreThisOnesThreadLocal = new ThreadLocal<Boolean>();

    final void runTests(final AbstractProcess<?, ?> process, final ProcessEntry processEntry) {

        checkNotNull(process);

        final ProcessInstance processInstance = getInitializedProcessInstance(process);

        for (final Method testMethod : testMethods) {

            final WhileProcessing whileProcessing = testMethod.getAnnotation(WhileProcessing.class);

            final WillReportAfterProcesses willReportAfterProcesses = testMethod
                    .getAnnotation(WillReportAfterProcesses.class);

            if (willReportAfterProcesses != null || whileProcessing == null) {
                continue;
            }

            final Class<? extends AbstractProcess<?, ?>>[] whileProcessingProcesses = whileProcessing.value();

            if (whileProcessingProcesses != null && whileProcessingProcesses.length != 0) {

                boolean found = false;

                for (final Class<? extends AbstractProcess<?, ?>> c : whileProcessingProcesses) {
                    if (c.isAssignableFrom(process.getClass())) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    continue;
                }
            }

            final String testMethodName = testMethod.getName();

            final List<Throwable> errors = processInstance.methodErrors.get(testMethodName);

            final List<AssertionError> failures = processInstance.methodFailures.get(testMethodName);

            final List<ProcessEntryResult> results = processInstance.processEntryResults.get(testMethodName);

            final ProcessEntryResult result = (processEntry == null) ? null : new ProcessEntryResult(processEntry);

            final long start = System.currentTimeMillis();

            ignoreThisOnesThreadLocal.set(false);

            try {

                testMethod.invoke(this);

            } catch (final InvocationTargetException e) {

                final Throwable cause = e.getTargetException();

                if (cause == null) {

                    setResultError(result, e);

                    errors.add(e);

                } else if (AssertionError.class.isInstance(cause)) {

                    setResultFailure(result, (AssertionError) cause);

                    failures.add((AssertionError) cause);

                } else {

                    setResultError(result, e);

                    errors.add(cause);
                }

            } catch (final AssertionError e) {

                setResultFailure(result, (AssertionError) e);

                failures.add(e);

            } catch (final Throwable e) {

                setResultError(result, e);

                errors.add(e);
            }

            if (result != null && !ignoreThisOnesThreadLocal.get()) {

                results.add(result);
            }

            final long elapsed = System.currentTimeMillis() - start;

            setResultElapsedMs(result, elapsed);
        }
    }

    private static void setResultError(final ProcessEntryResult result, final Throwable e) {

        if (result != null) {

            result.setError(e);
        }
    }

    private static void setResultFailure(final ProcessEntryResult result, final AssertionError e) {

        if (result != null) {

            result.setFailure(e);
        }
    }

    private static void setResultElapsedMs(final ProcessEntryResult result, final long elapsed) {

        if (result != null) {

            result.setElapsedMs(elapsed);
        }
    }

    private void emitStandardReport(final ProcessInstance processInstance) throws IllegalArgumentException,
            SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {

        final String processName = processInstance.process.getClass().getSimpleName();

        final Report report = instantiateReport(processName, 1, getReportCount());

        try {

            final int errorCount = processInstance.errors.size();

            switch (errorCount) {
            case 0:
                report.info("No error at process level.");
                break;
            case 1:
                report.error("One error at process level.");
                break;
            default:
                report.error(errorCount + " errors at process level.");
                break;
            }

            final List<Throwable> totalMethodErrors = new ArrayList<Throwable>();
            final List<AssertionError> totalMethodFailures = new ArrayList<AssertionError>();

            final Set<String> testMethodNames = processInstance.methodErrors.keySet();

            for (final String methodName : testMethodNames) {

                totalMethodErrors.addAll(processInstance.methodErrors.get(methodName));
                totalMethodFailures.addAll(processInstance.methodFailures.get(methodName));
            }

            final int totalMethodErrorCount = totalMethodErrors.size();
            final int totalMethodFailureCount = totalMethodFailures.size();

            switch (totalMethodFailureCount) {
            case 0:
                report.info("No failure at test level.");
                break;
            case 1:
                report.error("One failure at test level.");
                break;
            default:
                report.error(totalMethodFailureCount + " failures at test level.");
                break;
            }

            switch (totalMethodErrorCount) {
            case 0:
                report.info("No error at test level.");
                break;
            case 1:
                report.error("One error at test level.");
                break;
            default:
                report.error(totalMethodErrorCount + " errors at test level.");
                break;
            }

            // SUMMARY BY TEST METHOD

            report.info("Summary:");

            report.info("Test Method", "Runs", "Failures", "Errors");

            final int count = processInstance.process.getCurrentIndex();

            for (final String testMethodName : new TreeSet<String>(testMethodNames)) {

                final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);

                final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);

                report.infoDetail(testMethodName + "()", count, methodFailures.size(), methodErrors.size());
            }

            // DETAIL AT THE GLOBAL LEVEL

            // report.info("Errors at process level: " + errorCount);
            // for (int i = 0; i < errorCount && i < MAX; ++i) {
            // report.errorDetail(processInstance.errors.get(i));
            // }
            // if (errorCount > MAX) {
            // report.errorDetail("(...)");
            // }
            //
            // report.info("Failures at test level: " +
            // totalMethodFailureCount);
            // for (int i = 0; i < totalMethodFailureCount && i < MAX; ++i) {
            // report.errorDetail(totalMethodFailures.get(i));
            // }
            // if (totalMethodFailureCount > MAX) {
            // report.errorDetail("(...)");
            // }
            //
            // report.info("Errors at test level: " + totalMethodErrorCount);
            // for (int i = 0; i < totalMethodErrorCount && i < MAX; ++i) {
            // report.errorDetail(totalMethodErrors.get(i));
            // }
            // if (totalMethodErrorCount > MAX) {
            // report.errorDetail("(...)");
            // }

            // DETAIL BY TEST METHOD

            for (final String testMethodName : new TreeSet<String>(testMethodNames)) {

                final String summary = composeTestSummary(processInstance, testMethodName);

                final List<Throwable> methodErrors = processInstance.methodErrors.get(testMethodName);

                final List<AssertionError> methodFailures = processInstance.methodFailures.get(testMethodName);

                if (methodErrors.isEmpty() && methodFailures.isEmpty()) {

                    // report.info(testMethodName + "(): " + summary);

                } else {

                    report.error(testMethodName + "(): " + summary);
                    for (int i = 0; i < methodFailures.size() && i < MAX; ++i) {
                        report.errorDetail(methodFailures.get(i));
                    }
                    if (methodFailures.size() > MAX) {
                        report.errorDetail("(...)");
                    }

                    for (int i = 0; i < methodErrors.size() && i < MAX; ++i) {
                        report.errorDetail(methodErrors.get(i));
                    }
                    if (methodErrors.size() > MAX) {
                        report.errorDetail("(...)");
                    }
                }
            }

        } finally {

            report.send();
        }
    }

    private static void emitProcessEntryReport(final ProcessInstance processInstance) throws IOException {

        final File file = new File("target/processentry-report/" + processInstance.process.getClass().getSimpleName()
                + ".xml");

        FileUtils.forceMkdir(file.getParentFile());

        final OutputStream os = new FileOutputStream(file);
        try {

            final PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, UTF_8));

            pw.println("<processentry-report>");

            for (final Map.Entry<String, List<ProcessEntryResult>> e : processInstance.processEntryResults.entrySet()) {

                final String testMethodName = e.getKey();

                final List<ProcessEntryResult> results = e.getValue();

                pw.println("<testMethod name=\"" + testMethodName + "\">");

                if (results != null) {

                    for (final ProcessEntryResult result : results) {

                        pw.print("\t<processentry id=\"" + xmlEncode(result.getProcessEntryId()) + "\" elapsed=\""
                                + result.getElapsedMs() + "\"");

                        printReportXmlAttribute(pw, "errorClassName", result.getErrorClassName());
                        printReportXmlAttribute(pw, "errorMessage", result.getErrorMessage());
                        printReportXmlAttribute(pw, "failureClassName", result.getFailureClassName());
                        printReportXmlAttribute(pw, "failureMessage", result.getFailureMessage());

                        pw.println("/>");
                    }
                }

                pw.println("</testMethod>");
            }

            pw.println("</processentry-report>");

            pw.flush();

        } finally {
            os.close();
        }
    }

    private static void printReportXmlAttribute(final PrintWriter pw, final String name, final String value) {

        if (value != null) {
            pw.print(" " + name + "=\"" + xmlEncode(value) + "\"");
        }
    }

    private static String xmlEncode(final String s) {

        return s.replace("&", "&amp;").replace("\"", "&quot;").replace("<", "&lt;").replace(">", "&gt;");
    }

    private static final int MAX = 10;

    private int reportIndex = 2;

    protected final Report createReport() {

        final String testMethodName = getCurrentTestMethodName();

        final Report report;

        try {

            report = instantiateReport(getClass().getSimpleName() + "." + testMethodName + "()", reportIndex,
                    getReportCount());

        } catch (final RuntimeException e) {
            throw e;
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }

        ++reportIndex;

        return report;
    }

    private final int getReportCount() {

        int reportCount = 1;

        for (final Method testMethod : testMethods) {

            if (testMethod.isAnnotationPresent(WillReportAfterProcesses.class)) {

                ++reportCount;
            }
        }

        return reportCount;
    }

    private static boolean hasConstructor(final Class<?> c, final Class<?>... params) {

        try {

            c.getConstructor(params);

            return true;

        } catch (final Exception e) {

            // do nothing
        }

        return false;
    }

    private Report instantiateReport(final String name, final int index, final int count)
            throws IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {

        final Class<? extends TestsWithProcessesBefore> thisClass = getClass();

        final ReportTo reportTo = thisClass.getAnnotation(ReportTo.class);

        if (reportTo == null) {

            return new SystemOutReport(name, index, count);

        } else {

            final List<Report> reports = new ArrayList<Report>();

            for (final Class<? extends Report> reportClass : reportTo.value()) {

                final Report r;

                if (hasConstructor(reportClass, String.class, int.class, int.class)) {

                    r = reportClass.getConstructor(String.class, int.class, int.class).newInstance(name, index, count);

                } else if (hasConstructor(reportClass, String.class)) {

                    r = reportClass.getConstructor(String.class).newInstance(name);

                } else {

                    r = reportClass.newInstance();
                }

                reports.add(r);
            }

            return new CompositeReport(reports);
        }
    }
}
